﻿
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Threading;

namespace Red.Controls.Shell
{
    /// <summary>
    ///     Converter for use with exclusive-selection scenarios.
    /// </summary>
    public class RadioSelectionConverter : IValueConverter
    {
        public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
        {
            return value.Equals( parameter );
        }

        public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
        {
            return parameter;
        }
    }

    internal class ModeMenuBinding : Binding
    {
        public ModeMenuBinding( Drawer.Mode mode )
        {
            Path = new PropertyPath("DockedMode");
            RelativeSource = new RelativeSource() { AncestorType = typeof( Drawer ) };
            Converter = new RadioSelectionConverter();
            ConverterParameter = mode;
        }
    }

    public partial class DrawerControl : UserControl
    {
        public Drawer Owner { get; set; }

        public DrawerControl()
        {
            InitializeComponent();
        }

        private void TabItem_PreviewMouseDown( object sender, MouseButtonEventArgs e )
        {
            Owner.IsFolded = false;
        }
    }

    /// <summary>
    ///     Dockable panel with switchable pages (tabs)
    /// </summary>
    [ContentProperty( "Content" )]
    public class Drawer : ElementFacade<DrawerControl>
    {
        #region Embedded types
        
        public enum Mode
        {
            Floating,
            Docked,
            Folding
        }

        #endregion Embedded types

        #region Data

        private const int _undockedOffsetX = 10;
        private const int _undockedOffsetY = 10;

        private Mode _mode = Mode.Docked;
        private bool _dockPropertyChangedPending = false;
        private Panel _dockedPanel;
        private Panel _overlayPanel;
        private Grid _dockGrid;
        private GridSplitter _splitter;
        private DrawerWindow _floatingWindow;
        private Rect? _layoutToSetAfterApplyingMode;

        private double _unfoldedSize;
        private double _designedWidth;
        private double _designedHeight;
        private double _designedMinWidth;
        private double _designedMinHeight;

        #endregion Data

        #region Properties

        public new static readonly DependencyProperty ContentProperty =
            DependencyProperty.Register( "Content", typeof( ObservableCollection<Page> ), typeof( Drawer ),
                new FrameworkPropertyMetadata( null ) );

        public static readonly DependencyProperty DockProperty = // It's Nullable to force the first set trigger OnChanged event
            DependencyProperty.Register( "Dock", typeof( Dock? ), typeof( Drawer ),
                new FrameworkPropertyMetadata( null, DockPropertyChanged ), IsDockPropertyValid );

        private static readonly DependencyPropertyKey OrientationPropertyKey =
            DependencyProperty.RegisterReadOnly( "Orientation", typeof( Orientation? ), typeof( Drawer ),
                new FrameworkPropertyMetadata( null ) );

        public static readonly DependencyProperty OrientationProperty = OrientationPropertyKey.DependencyProperty;

        public static readonly DependencyProperty DockedModeProperty =
            DependencyProperty.Register( "DockedMode", typeof( Mode ), typeof( Drawer ),
                new FrameworkPropertyMetadata( Mode.Docked, OnDockedModePropertyChanged ) );

        public static readonly DependencyProperty IsFoldedProperty =
            DependencyProperty.Register( "Folded", typeof( bool ), typeof( Drawer ),
                new FrameworkPropertyMetadata( false, OnFoldedPropertyChanged ) );

        /// <summary>
        ///     The content of a drawer - collection of pages
        /// </summary>
        public new ObservableCollection<Page> Content
        {
            get { return (ObservableCollection<Page>)GetValue( ContentProperty ); }
            set { SetValue( ContentProperty, value ); }
        }

        /// <summary>
        ///     Where to dock the drawer. Supported values are Left, Right and Bottom. Describes the 'desired' home for
        ///     the drawer, so keeps its value even if the drawer is floating
        /// </summary>
        public Dock Dock
        {
            get { return (Dock)GetValue( DockProperty ); }
            set { SetValue( DockProperty, value ); }
        }

        /// <summary>
        ///     Read only property that describes if the drawer is Horizontal (docked at Bottom) or Vertical (Left or Right)
        /// </summary>
        public Orientation Orientation
        {
            get { return (Orientation)GetValue( OrientationProperty ); }
            private set { SetValue( OrientationPropertyKey, value ); }
        }

        /// <summary>
        ///     Gets or sets the actual drawer mode
        /// </summary>
        public Mode DockedMode
        {
            get { return (Mode)GetValue( DockedModeProperty ); }
            set { SetValue( DockedModeProperty, value ); }
        }

        /// <summary>
        ///     When in Folding mode, it allows to get/set if the drawer is folded or shown. In other modes has no effect.
        /// </summary>
        public bool IsFolded
        {
            get { return (bool)GetValue( IsFoldedProperty ); }
            set { SetValue( IsFoldedProperty, value ); }
        }

        /// <summary>
        ///     Gets or sets the active (visible) page.
        /// </summary>
        /// <exception cref="ArgumentException">Invalid page, or page does not exist in drawer</exception>
        public Page ActivePage
        {
            get
            {
                var item = Element.Tabs.SelectedItem as TabItem;
                return item.Content as Page;
            }

            set
            {
                foreach ( TabItem item in Element.Tabs.Items )
                {
                    if ( item.Content == value )
                    {
                        Element.Tabs.SelectedItem = item;
                        return;
                    }
                }

                throw new ArgumentException( "Invalid page, or page does not exist in drawer" );
            }
        }

        #endregion Properties

        #region C-tor

        public Drawer()
        {
            Element.Owner = this;
            Content = new ObservableCollection<Page>(); // WARNING: This CANNOT be put as a property default value - because then all drawers would share the same collection
            Content.CollectionChanged += Pages_CollectionChanged;
            Element.MinWidth = Element.Tabs.MinWidth;
            Element.MinHeight = Element.Tabs.MinHeight;

            Initialized += delegate
            {
                // Store original layout aside to be able to reset to it later
                _designedWidth = Width;
                _designedHeight = Height;
                _designedMinWidth = MinWidth;
                _designedMinHeight = MinHeight;
                // Zeroing minimums is required for the folding to work
                MinWidth = 0;
                MinHeight = 0;
            };
        }

        #endregion C-tor

        #region Events

        internal class RelocationEventArgs : EventArgs
        {
            public RelocationEventArgs( Dock location )
            {
                RequestedLocation = location;
            }

            public Dock RequestedLocation { get; private set; }

            public Panel DestinationPanel { get; set; }

            public GridSplitter DestinationSplitter { get; set; }

            public Panel OverlayPanel { get; set; }

            public Grid DockGrid { get; set; }
        }

        internal delegate void RelocationRequestHandler( Drawer sender, RelocationEventArgs args );

        internal event RelocationRequestHandler RelocationRequest;

        #endregion

        #region Internal methods

        internal bool ApplyLayoutString( string drawerLayout )
        {
            FinalizeDockPropertyChanged(); // ensure consistent state

            string[] tokens = drawerLayout.Split( new[] { ',' } );
            if ( tokens.Length != 6 )
            {
                return false;
            }

            Mode mode;
            if ( !Enum.TryParse<Mode>( tokens[ 0 ], out mode ) )
            {
                mode = Mode.Docked; // default to docked
            }

            double w, h, x, y;
            bool sizeOk = Double.TryParse( tokens[ 1 ], out w );
            sizeOk = Double.TryParse( tokens[ 2 ], out h ) && sizeOk;
            bool posOk = Double.TryParse( tokens[ 3 ], out x );
            posOk = Double.TryParse( tokens[ 4 ], out y ) && posOk;

            var isListedStr = tokens[ 5 ];
            if ( isListedStr.Length == Content.Count )
            {
                for ( int i = 0; i < Content.Count; ++i )
                {
                    Content[i].IsListed = ( isListedStr[i] == 'o' );
                }
            }

            // Try to apply the layout only if all needed values are successfully read. Otherwise it's better to keep the default layout.
            if ( ( mode != Mode.Floating && sizeOk ) || ( mode == Mode.Floating && posOk && sizeOk ) )
            {
                if ( DockedMode == mode )
                {
                    ApplyLayout( new Rect( x, y, w, h ) );
                }
                else
                {
                    _layoutToSetAfterApplyingMode = new Rect( x, y, w, h );
                    DockedMode = mode;
                }
                return true;
            }

            return false;
        }

        internal string GetLayoutString()
        {
            string leftStr = "";
            string topStr = "";
            string modeStr = _mode.ToString();
            string isListedStr = "";

            if ( _floatingWindow != null )
            { // store relative position so that the string is main windows position independent
                Point drawerScrPos = _floatingWindow.PointToScreen( new Point( 0, 0 ) );
                double relativePosX = drawerScrPos.X - _floatingWindow.Owner.Left;
                double relativePosY = drawerScrPos.Y - _floatingWindow.Owner.Top;
                leftStr = relativePosX.ToString();
                topStr = relativePosY.ToString();
            }

            foreach ( Page page in Content )
            {
                isListedStr += page.IsListed ? 'o' : 'x';
            }

            return String.Join( ",", new []{
                modeStr,
                (( DockedMode == Mode.Folding && Orientation == Orientation.Vertical ) ? (int)_unfoldedSize : (int)ActualWidth).ToString(),
                (( DockedMode == Mode.Folding && Orientation == Orientation.Horizontal ) ? (int)_unfoldedSize : (int)ActualHeight).ToString(),
                leftStr,
                topStr,
                isListedStr
            } );
        }

        internal void ResetLayout()
        {
            FinalizeDockPropertyChanged(); // ensure consistent state
            DoDock();
            ApplyLayout( new Rect( 0, 0, _designedWidth, _designedHeight ) );
        }

        #endregion Internal methods

        #region Private methods

        private void AttachToDockedPanel()
        {
            Debug.Assert( _dockedPanel != null );
            _dockedPanel.Children.Add( this );
            _dockedPanel.Visibility = Visibility.Visible;
            _splitter.Visibility = Visibility.Visible;
            HorizontalAlignment = HorizontalAlignment.Stretch;
            VerticalAlignment = VerticalAlignment.Stretch;
        }

        private void DetachFromDockedPanel()
        {
            if ( _dockedPanel != null && _dockedPanel.Children.Contains( this ) )
            {
                SetDockedCellSize( Double.NaN, Double.NaN, 0, 0 );
                _dockedPanel.Children.Remove( this );
                _dockedPanel.Visibility = Visibility.Collapsed;
                _splitter.Visibility = Visibility.Collapsed;
            }
        }

        private void AttachToOverlayPanel()
        {
            Debug.Assert( _overlayPanel != null );
            SetDockedCellSize( Double.NaN, Double.NaN, 0, 0 );
            Thickness margin = _dockGrid.Margin;
            switch ( Dock )
            {
                case Dock.Left:
                    HorizontalAlignment = HorizontalAlignment.Left;
                    VerticalAlignment = VerticalAlignment.Stretch;
                    _dockGrid.Margin = new Thickness( 25, margin.Top, margin.Right, margin.Bottom ); // move aside the center element
                    break;
                case Dock.Right:
                    HorizontalAlignment = HorizontalAlignment.Right;
                    VerticalAlignment = VerticalAlignment.Stretch;
                    _dockGrid.Margin = new Thickness( margin.Left, margin.Top, 25, margin.Bottom );
                    break;
                case Dock.Bottom:
                    HorizontalAlignment = HorizontalAlignment.Stretch;
                    VerticalAlignment = VerticalAlignment.Bottom;
                    _dockGrid.Margin = new Thickness( margin.Left, margin.Top, margin.Right, 25 );
                    break;
            }
            _overlayPanel.Children.Add( this );
        }

        private void DetachFromOverlayPanel()
        {
            if ( _overlayPanel != null && _overlayPanel.Children.Contains( this ) )
            {
                SetDockedCellSize( Double.NaN, Double.NaN, 0, 0 );
                _overlayPanel.Children.Remove( this );
            }
            Element.Border.BorderThickness = new Thickness( 0 );
            Thickness margin = _dockGrid.Margin;
            switch ( Dock )
            {
                case Dock.Left:
                    _dockGrid.Margin = new Thickness( 0, margin.Top, margin.Right, margin.Bottom );
                    break;
                case Dock.Right:
                    _dockGrid.Margin = new Thickness( margin.Left, margin.Top, 0, margin.Bottom );
                    break;
                case Dock.Bottom:
                    _dockGrid.Margin = new Thickness( margin.Left, margin.Top, margin.Right, 0 );
                    break;
            }
        }

        private void AttachToWindow()
        {
            HorizontalAlignment = HorizontalAlignment.Stretch;
            VerticalAlignment = VerticalAlignment.Stretch;
            var topWindow = Window.GetWindow( _dockedPanel ); // do not try to get window from 'this' - may be detached!
            _floatingWindow = new DrawerWindow()
            {
                Content = this,
                Owner = topWindow
            };
            Dispatcher.BeginInvoke( (Action)delegate
            {
                _floatingWindow.Show();
            } );
        }

        private void DetachFromWindow()
        {
            if ( _floatingWindow != null )
            {
                _floatingWindow.Content = null;
                _floatingWindow.Hide();
                _floatingWindow = null;
            }
        }

        private void DoFloat()
        {
            if ( _mode == Mode.Floating )
            {
                return;
            }

            _mode = Mode.Floating;

            DetachFromDockedPanel();
            DetachFromOverlayPanel();
            AttachToWindow();
        }

        private void DoDock()
        {
            if ( _mode == Mode.Docked )
            {
                return;
            }

            _mode = Mode.Docked;

            DetachFromWindow();
            DetachFromOverlayPanel();
            AttachToDockedPanel();
        }

        private void DoFold()
        {
            if ( _mode == Mode.Folding )
            {
                return;
            }

            _mode = Mode.Folding;

            DetachFromWindow();
            DetachFromDockedPanel();
            AttachToOverlayPanel();
        }

        // Adjusts drawer's position and size, taking into consideration the current mode
        // NOTE 1: x and y are the drawer window's position and ignored if the drawer is docked
        // NOTE 2: x and y are relative to the owner's (MainWindow) position
        private void ApplyLayout( Rect layout )
        {
            switch ( _mode )
            {
                case Mode.Floating:
                    Debug.Assert( _floatingWindow != null && _floatingWindow.Owner != null );
                    _floatingWindow.Left = layout.X + _floatingWindow.Owner.Left;
                    _floatingWindow.Top = layout.Y + _floatingWindow.Owner.Top;
                    Width = layout.Width; // temporarily set Width and Height to non-auto to allow SizeToContent do its work
                    Height = layout.Height; // (it's the only way I found to trustworthy to set 'client' window size)
                    _floatingWindow.SizeToContent = SizeToContent.WidthAndHeight;
                    _floatingWindow.Dispatcher.BeginInvoke( (Action)delegate
                    {
                        _floatingWindow.SizeToContent = SizeToContent.Manual;
                        Width = Double.NaN; // re-enable drawer auto size
                        Height = Double.NaN;
                    });
                    break;

                case Mode.Docked:
                    Width = Double.NaN;
                    Height = Double.NaN;
                    SetDockedCellSize( layout.Width, layout.Height, _designedMinWidth + 25, MinHeight + 25 );
                    break;

                case Mode.Folding:
                    _unfoldedSize = ( Orientation == Orientation.Vertical ) ? layout.Width : layout.Height;
                    break;
            }
        }

        private void FinalizeDockPropertyChanged()
        {
            if ( _dockPropertyChangedPending )
            {
                _dockPropertyChangedPending = false;
                // Obtaining the actual w and h to set must be prepared for different scenarios what's been set at the design time
                var w = ( ActualWidth == 0 ) ? ( Double.IsNaN( Width ) ? _designedMinWidth : Width ) : ActualWidth;
                var h = ( ActualHeight == 0 ) ? ( Double.IsNaN( Height ) ? _designedMinWidth : Height ) : ActualHeight;
                AttachToDockedPanel();
                ApplyLayout( new Rect( 0, 0, w, h ) );
            }
        }

        private void ApplyMode( Mode oldMode, Mode mode )
        {
            if ( _layoutToSetAfterApplyingMode == null && !IsLoaded )
            { // Window.GetWindow and PointToScreen will fail if not fully loaded
                Dispatcher.BeginInvoke( (Action)delegate { ApplyMode( oldMode, mode ); }, DispatcherPriority.Background );
                return;
            }

            if ( _layoutToSetAfterApplyingMode == null )
            {
                var topWindow = Window.GetWindow( this );
                Point screenPos = Element.PointToScreen( new Point( 0, 0 ) );
                _layoutToSetAfterApplyingMode = new Rect(
                    screenPos.X + ( Dock == Dock.Left ? -_undockedOffsetX : _undockedOffsetX ) - topWindow.Left,
                    screenPos.Y + ( Dock == Dock.Bottom ? _undockedOffsetY : -_undockedOffsetY ) - topWindow.Top,
                    ( oldMode == Mode.Folding && Orientation == Orientation.Vertical ) ? _unfoldedSize : ActualWidth,
                    ( oldMode == Mode.Folding && Orientation == Orientation.Horizontal ) ? _unfoldedSize : ActualHeight
                    );
            }

            if ( oldMode == Mode.Folding )
            {
                IsFolded = false;
            }

            switch ( mode )
            {
                case Mode.Floating:
                    DoFloat();
                    break;
                case Mode.Docked:
                    DoDock();
                    break;
                case Mode.Folding:
                    DoFold();
                    break;
            }

            ApplyLayout( _layoutToSetAfterApplyingMode.Value );
            _layoutToSetAfterApplyingMode = null;

            if ( mode == Mode.Folding )
            {
                IsFolded = true;
            }
        }

        private void ApplyDock( Dock dock )
        {
            if ( RelocationRequest == null )
                return; // can be null on design time

            RelocationEventArgs args = new RelocationEventArgs( dock );
            RelocationRequest( this, args ); // ask frame for the container
            Debug.Assert( args.DestinationPanel != null );
            if ( _mode == Mode.Docked )
            {
                DetachFromDockedPanel();
            }

            _dockedPanel = args.DestinationPanel;
            _overlayPanel = args.OverlayPanel;
            _dockGrid = args.DockGrid;
            _splitter = args.DestinationSplitter;

            if ( _mode == Mode.Docked )
            {
                // Queue the action in case the setter is called during the initialization (which can later override properties being set there)
                _dockPropertyChangedPending = true;
                Dispatcher.BeginInvoke( (Action)delegate
                {
                    FinalizeDockPropertyChanged();
                } );
            }

            Orientation = ( dock == Dock.Left || dock == Dock.Right ) ? Orientation.Vertical : Orientation.Horizontal;
        }

        // Helper method to set the size of a docked cell - be it a column or a row, depending on a drawer's placement
        private void SetDockedCellSize( double w, double h, double minW, double minH )
        {
            if ( Orientation == Orientation.Vertical )
            {
                int column = Grid.GetColumn( _dockedPanel );
                _dockGrid.ColumnDefinitions[ column ].Width = Double.IsNaN(w) ? GridLength.Auto : new GridLength( w );
                _dockGrid.ColumnDefinitions[ column ].MinWidth = minW;
            }
            else
            {
                int row = Grid.GetRow( _dockedPanel );
                _dockGrid.RowDefinitions[ row ].Height = Double.IsNaN( h ) ? GridLength.Auto : new GridLength( h );
                _dockGrid.RowDefinitions[ row ].MinHeight = minH;
            }
        }

        private void ApplyIsFolded( bool fold )
        {
            foreach ( Page page in Content )
            {
                page.Visibility = fold ? Visibility.Collapsed : Visibility.Visible;
            }

            if ( fold )
            {
                Element.Border.BorderThickness = new Thickness( 0 );
            }
            else
            {
                switch ( Dock )
                {
                    case Dock.Left:
                        Element.Border.BorderThickness = new Thickness( 0, 0, 1, 0 );
                        break;
                    case Dock.Right:
                        Element.Border.BorderThickness = new Thickness( 1, 0, 0, 0 );
                        break;
                    case Dock.Bottom:
                        Element.Border.BorderThickness = new Thickness( 0, 1, 0, 0 );
                        break;
                }
            }

            if ( Orientation == Orientation.Vertical )
            {
                Width = fold ? Double.NaN : _unfoldedSize;
            }
            else
            {
                Height = fold ? Double.NaN : _unfoldedSize;
            }
        }

        void AddPageTab( Page page )
        {
            var item = new TabItem { Content = page };
            item.SetBinding( TabItem.HeaderProperty, new Binding( "Title" ) { Source = page } );
            page.IsListedChanged += Page_IsListedChanged;
            Element.Tabs.Items.Add( item );
        }

        #endregion Private methods

        #region Event handlers

        private void Pages_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            switch ( e.Action )
            {
                case NotifyCollectionChangedAction.Add:
                    foreach ( object newItem in e.NewItems )
                    {
                        var page = newItem as Page;
                        AddPageTab( page );
                    }
                    break;
            }
        }

        void Page_IsListedChanged( object sender, EventArgs e )
        {
            // rebuild pages
            Element.Tabs.Items.Clear();
            foreach ( Page page in Content )
            {
                if ( page.IsListed )
                {
                    AddPageTab( page );
                }
            }
        }

        private static bool IsDockPropertyValid( object value )
        {
            return value == null || ( (Dock?)value ).Value != Dock.Top;
        }

        private static void DockPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Debug.Assert( e.NewValue != null );
            var drawer = (Drawer)d;
            var dock = (Dock)e.NewValue;
            drawer.ApplyDock( dock );
        }

        private static void OnDockedModePropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Debug.Assert( e.NewValue != null );
            Drawer drawer = (Drawer)d;
            Mode oldMode = (Mode)e.OldValue;
            Mode newMode = (Mode)e.NewValue;
            drawer.ApplyMode( oldMode, newMode );
        }

        private static void OnFoldedPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Drawer drawer = (Drawer)d;
            drawer.ApplyIsFolded( true.Equals( e.NewValue ) );
        }

        #endregion Event handlers

    }
}
